cimport _octodb
from python_string cimport PyString_FromStringAndSize
from stdlib cimport malloc,realloc,free

import copy

DEF QUERY_SELECT = 0
DEF QUERY_INSERT = 1
DEF QUERY_DELETE = 2
DEF QUERY_UPDATE = 3

#
# TODO:
#  - serialization function needs two different implementations:
#   1) serialization for conditions (destination type is assumed from source)
#   2) serialization for insert (destination is determined by known datatype and arguments)
#
#  With some data types, the two are pretty much the same, but with others, 
#  such as 'array', the distinction is crucial.

# Raw data.
cdef class OctoDBDataRaw:
	cdef char *data
	cdef size_t size
	def __init__(self, char *data, size_t size):
		self.data = data
		self.size = size

# Enforced data type.
class OctoDBDataForce:
	def __init__(self, value, datatype, dtargs):
		self.value = value
		self.datatype = datatype
		self.dtargs = dtargs

ctypedef signed long long int64_t
cdef int64_serialize(value_in, char **data, size_t *size):
	cdef int64_t value

	value = long(value_in)

	size[0] = sizeof(int64_t)
	if (size[0] != 8):
		raise ValueError
	data[0] = <char *>malloc(size[0])
	(<int64_t*>(data[0]))[0] = long(value)
	return True
	
cdef int64_deserialize(list dtargs, char *data, size_t size):
	if (size != 8):
		raise ValueError
	if (sizeof(int64_t) != size):
		raise ValueError
	py_int = (<int64_t *>data)[0]
	return py_int

cdef float_serialize(value_in, char **data, size_t *size):
	cdef double value

	value = float(value_in)

	size[0] = sizeof(double)
	if (size[0] != 8):
		raise ValueError
	data[0] = <char *>malloc(size[0])
	(<double*>(data[0]))[0] = long(value)
	return True
	
cdef float_deserialize(list dtargs, char *data, size_t size):
	if (size != 8):
		raise ValueError
	if (sizeof(double) != size):
		raise ValueError
	py_float = (<double *>data)[0]
	return py_float

cdef string_serialize(value_in, char **data, size_t *size):
	cdef char *value

	value_in = str(value_in)
	value = value_in

	size[0] = len(value) + 1
	data[0] = value
	return False

cdef string_deserialize(list dtargs, char *data, size_t size):
	py_str = PyString_FromStringAndSize(data, size)
	return py_str

ctypedef unsigned uint32_t
cdef array_serialize(value_in, char **data, size_t *size):
	cdef char *item_data
	cdef size_t item_size

	value_in = list(value_in)

	if sizeof(uint32_t) != 4:
		raise ValueError

	offset = 0
	size[0] = sizeof(uint32_t) + len(value_in) * sizeof(size_t)
	data[0] = <char*>malloc(size[0])
	(<uint32_t *>(&((data[0])[offset])))[0] = len(value_in)
	offset = sizeof(uint32_t)

	for item in value_in:
		# TODO: This might differ according to the dtargs of the array.
		# Right now, we do blind guess that it's homogenous.
		#
		# Perhaps the homogenity/validity check could be done beforehand.
		deallocate = octodb_serialize_condition(item, &item_data, &item_size)

		(<size_t *>(&((data[0])[offset])))[0] = item_size

		size[0] += item_size
		data[0] = <char*>realloc(data[0], size[0])

		offset += sizeof(size_t)
		memcpy(&((data[0])[offset]), item_data, item_size)
		offset += item_size

		if deallocate == True:
			free(item_data)
	return True

cdef array_deserialize(list dtargs, char *data, size_t size):
	cdef uint32_t items
	cdef char *item_data
	cdef size_t item_size

	# TODO, here we're gonna need to know the source data type for sure.
	# Ergo, we need to pass dtargs.
	datatype = dtargs[0]
	dtargs = dtargs[1:]

	if sizeof(uint32_t) != 4:
		raise ValueError

	output = []
	offset = 0
	items = (<uint32_t *>(&((data)[offset])))[0]
	offset += sizeof(uint32_t)
	for i in range(0, items):
		item_size = (<size_t *>(&((data)[offset])))[0]
		offset += sizeof(size_t)
		item_data = &data[offset]
		offset += item_size
		output.append(octodb_deserialize(datatype, dtargs, item_data, item_size))
	return output


#
# This function serializes data according to the type
# on input, i.e. a 'str' will get turned into a string
# etc.
#
# Since there is no type-awarness of condition arguments
# in OctoDB, this is the next best thing.
#
cdef octodb_serialize_condition(value, char **data, size_t *size):
	if value == None:
		data[0] = NULL
		size[0] = 0
		return False

	if isinstance(value, int) or isinstance(value, long):
		return int64_serialize(value, data, size)
	if isinstance(value, str):
		return string_serialize(value, data, size)
	if isinstance(value, OctoDBDataRaw):
		raw_data = value
		data[0] = raw_data.data
		size[0] = raw_data.size
		return False
	raise ValueError, 'Unable to determine the data type. Try passing raw binary data using OctoDBDataRaw.'

#
# This function, unlike octodb_serialize_condition, is aware
# of the data type of the column in which the insert is performed.
#
# The data therefore gets converted according to the data type
# of the column.
#
cdef octodb_serialize_insert(value, datatype, char **data, size_t *size):
	cdef OctoDBDataRaw raw_data

	#
	# The value may legitimately be None only for an update.
	# The set_data method should raise an exception, if
	# the query is insert and the value is None.
	#
	if value == None:
		data[0] = NULL
		size[0] = 0
		return False
	
	if isinstance(value, OctoDBDataForce):
		datatype = value.datatype
		dtargs = value.dtargs
		value = value.value #How delightful!

	if datatype == 'int64':
		return int64_serialize(value, data, size)
	if datatype == 'string':
		return string_serialize(value, data, size)
	if datatype == 'array':
		return array_serialize(value, data, size)
	if isinstance(value, OctoDBDataRaw):
		raw_data = value
		data[0] = raw_data.data
		size[0] = raw_data.size
		return False
	raise ValueError, 'Unable to serialize into the column data type. Try passing raw binary data using OctoDBDataRaw, or forcing the data type using OctoDBDataForce.'

cdef octodb_deserialize(datatype, dtargs, char *data, size_t size):
	dtargs = list(dtargs)

	# TODO: A dict-based lookup should be here, however, storing C callbacks in Cython
	# dict proved difficult.
	if datatype == 'int64':
		return int64_deserialize(dtargs, data, size)
	if datatype == 'string':
		return string_deserialize(dtargs, data, size)
	if datatype == 'array':
		return array_deserialize(dtargs, data, size)
	return OctoDBDataRaw(data, size)

##
# Exceptions
##

class OctoDBException(Exception):
	pass

class OctoDBExceptionDDL(OctoDBException):
	pass

class OctoDBExceptionResult(OctoDBException):
	def __init__(self, rvcode):
		Exception.__init__(self)
		self.rvcode = rvcode
		self.rvtext = result_text(rvcode)
	
	def __str__(self):
		return '<%s: %d="%s">' % (
			self.__class__.__name__,
			self.rvcode,
			self.rvtext,
		)

		

cdef class OctoDBDatabase:
	def __init__(self, char *path):
		self._path = path
	
	def __dealloc__(self):
		self.close()
	
	def open(self):
		self._database = database_open(self._path)
		if self._database == NULL:
			raise ValueError

	def table_create(self, OctoDBTable table):
		cdef table_t *_table

		if not isinstance(table, OctoDBTable):
			raise TypeError
		if self._database == NULL:
			raise ValueError

		table.bind()
		rv = database_table_create(self._database, table._table)
		if rv != 0:
			raise OctoDBExceptionDDL
	
	def table_get(self, char *tablename):
		table = OctoDBTable(tablename)
		table.parse(self)
		return table
	
	def table_drop(self, char *tablename):
		rv = database_table_drop(self._database, tablename)
		if rv != 0:
			raise OctoDBExceptionDDL

	def close(self):
		database_close(self._database)
		self._database = NULL



cdef class OctoDBTable:
	def __init__(self, name):
		self.name = name
		self.columns = []
		self._table = NULL
	
	def __dealloc__(self):
		table_destroy(self._table)
		self._table = NULL
	
	def parse(self, OctoDBDatabase database):
		cdef table_t *_table

		_table = database_table_get(<database_t *>database._database, self.name)
		if _table == NULL:
			raise OctoDBExceptionDDL

		# Name is already parsed, obviously. Columns follow.
		self.columns = []

		column_count = table_get_column_count(_table)
		for item in range(0, column_count):
			column_name = table_get_column_name(_table, item)
			column_datatype = table_get_column_datatype(_table, item)
			column_dtargs = []
			column = OctoDBColumn(column_name, column_datatype, column_dtargs)
	
	def add_column(self, column):
		if not isinstance(column, OctoDBColumn):
			raise TypeError
		self.columns.append(column)
	
	def _bind_column(self, OctoDBColumn column):
		column.bind()
		table_add_column(self._table, column._column)
		column.invalidate()

	def bind(self):
		self._table = table_new(<char *>self.name)
		for column in self.columns:
			self._bind_column(column)


cdef class OctoDBColumn:
	def __init__(self, name, datatype, *dtargs):
		self.name = name
		self.datatype = datatype
		self.dtargs = list(dtargs)

		self._column = NULL

	def __dealloc__(self):
		column_destroy(self._column)
		self._column = NULL

	def bind(self):
		cdef char **_dtargs
		
		_dtargs = <char **>malloc(sizeof(char *) * len(self.dtargs))
		for itemid, item in enumerate(self.dtargs):
			_dtargs[itemid] = <char *>item
		self._column = column_new_list(self.name, self.datatype, _dtargs, len(self.dtargs))
		free(_dtargs)
	
	def invalidate(self):
		self._column = NULL
		

cdef class OctoDBRow:
	def __init__(self, OctoDBQuery query):
		self._query = query._query
	
	def __getitem__(self, item):
		cdef char *data
		cdef size_t size
		cdef char *datatype

		cdef char **dtargs_raw
		cdef int dtargs_c

		rv = query_datatype_get(<query_t *>self._query, item, &datatype)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		rv = query_datatype_args_get(<query_t *>self._query, item, &dtargs_raw, &dtargs_c)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		rv = query_data_get(<query_t *>self._query, item, &data, &size)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		
		dtargs = []
		for i in range(0, dtargs_c):
			dtargs.append(dtargs_raw[i])

		py_value = octodb_deserialize(datatype, dtargs, data, size)
		return py_value


cdef class OctoDBTransaction:
	def __cinit__(self, OctoDBDatabase database, isolation):
		self._transaction = NULL
		self._database = database._database
		self.isolation = isolation
		self.begin()
	
	def __dealloc__(self):
		if self._transaction != NULL:
			self.rollback()
	
	def begin(self):
		self._transaction = transaction_begin(self._database, <int>self.isolation)

	def commit(self):
		transaction_commit(self._transaction)
		self._transaction = NULL

	def rollback(self):
		transaction_rollback(self._transaction)
		self._transaction = NULL


cdef class OctoDBCondition:
	def __invert__(self):
		obj = copy.copy(self)
		obj.positive = not obj.positive
		return obj
	__neg__ = __invert__


cdef class OctoDBConditionPredicate(OctoDBCondition):
	def __init__(self, column_id, predname, *args):
		self.positive = True
		self._condition = NULL

		self.column_id = column_id
		self.predname = predname
		self.args = list(args)
	
	def __dealloc__(self):
		condition_destroy(self._condition)
		self._condition = NULL
	
	def bind(self, OctoDBQuery query):
		cdef char *data = NULL
		cdef size_t size = 0
		cdef condition_t *_condition
		cdef char **rawdatas
		cdef size_t *lengths

		rawdatas = <char **>malloc(sizeof(char*) * len(self.args))
		lengths = <size_t *>malloc(sizeof(size_t) * len(self.args))

		for itemid,item in enumerate(self.args):
			octodb_serialize_condition(item, &data, &size)
			if data == NULL and size > 0:
				raise ValueError

			rawdatas[itemid] = data
			lengths[itemid] = size
		self._condition = condition_predicate_new_list(self.column_id, self.predname, len(self.args), lengths, rawdatas)
		free(rawdatas)
		free(lengths)

	def invalidate(self):
		self._condition = NULL

cdef int _query_callback_caller(query_t *query, void *user_data):
	(functor_obj, query_obj, row_obj) = <object>user_data
	rv = functor_obj(query_obj, row_obj)
	return <int>rv

cdef class OctoDBConditionCallback(OctoDBCondition):
	def __init__(self, callback):
		self.positive = True
		self._condition = NULL
		self.callback = callback
	
	def __dealloc__(self):
		condition_destroy(self._condition)
		self._condition = NULL
	
	def bind(self, OctoDBQuery query):
		cdef condition_t *_condition

		self.callback_data = (self.callback, query, OctoDBRow(query))
		self._condition = condition_callback_new(_query_callback_caller, <void *>self.callback_data)

	def invalidate(self):
		self._condition = NULL
	
cdef _set_condition(condition_t **_condition_ptr, OctoDBCondition condition):
	_condition_ptr[0] = condition._condition
	condition.invalidate()

cdef class OctoDBQuery:
	def __init__(self, querytype, OctoDBTransaction transaction, tablename):
		self._query = query_new(<transaction_t *>transaction._transaction)
		rv = query_prepare(self._query, querytype, tablename)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		self._executed = False
		self.conditions = []  # We need to hold the objects to prevent GC.

	def __dealloc__(self):
		query_destroy(self._query)
	
	def execute(self):
		rv = query_execute(self._query)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		self._executed = True
	
	def step(self):
		rv = query_step(self._query)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
			
	def finalize(self):
		rv = query_finalize(self._query)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)

	def __iter__(self):
		if not self._executed:
			self.execute()
		return self

	def __next__(self):
		rv = query_step(self._query)
		if result_error(rv):
			raise OctoDBExceptionResult(rv)
		if result_eod(rv):
			raise StopIteration
		return OctoDBRow(self)
	
	def set_data(self, *data_in):
		cdef char *data = NULL
		cdef size_t size = 0
		cdef condition_t *_condition
		cdef char **rawdatas
		cdef size_t *lengths
		cdef char *datatype

		rawdatas = <char **>malloc(sizeof(char*) * len(data_in))
		lengths = <size_t *>malloc(sizeof(size_t) * len(data_in))

		releaselist = []
		for itemid, item in enumerate(data_in):
			# TODO:
			#  - pass datatype args, too
			#  - prevent item to be None for INSERT.
			rv = query_datatype_get(<query_t *>self._query, itemid, &datatype)
			if result_error(rv):
				raise OctoDBExceptionResult(rv)

			# Return value of octodb_serialize always tells if
			# we are supposed to release the memory after we've
			# passed it on.
			release = octodb_serialize_insert(item, datatype, &data, &size)
			if data == NULL and size > 0:
				raise ValueError

			if release == True:
				releaselist.append(itemid)

			rawdatas[itemid] = data
			lengths[itemid] = size
		query_data_set_list(self._query, rawdatas, lengths)

		for release in releaselist:
			free(rawdatas[release])
		free(rawdatas)
		free(lengths)
		
	
	def add_conditions(self, *conditions):
		cdef condition_t **_condition_list
		_condition_list = <condition_t **>malloc(sizeof(condition_t *) * 1)
		for conditionid, condition in enumerate(conditions):
			condition.bind(self)
			_set_condition(&(_condition_list[conditionid]), condition)
		query_append_conditions_list(self._query, len(conditions), _condition_list)
		self.conditions.append(conditions)


cdef class OctoDBQuerySelect(OctoDBQuery):
	def __init__(self, OctoDBTransaction transaction, tablename):
		OctoDBQuery.__init__(self, QUERY_SELECT, transaction, tablename)

cdef class OctoDBQueryInsert(OctoDBQuery):
	def __init__(self, OctoDBTransaction transaction, tablename):
		OctoDBQuery.__init__(self, QUERY_INSERT, transaction, tablename)

cdef class OctoDBQueryUpdate(OctoDBQuery):
	def __init__(self, OctoDBTransaction transaction, tablename):
		OctoDBQuery.__init__(self, QUERY_UPDATE, transaction, tablename)

cdef class OctoDBQueryDelete(OctoDBQuery):
	def __init__(self, OctoDBTransaction transaction, tablename):
		OctoDBQuery.__init__(self, QUERY_DELETE, transaction, tablename)
